// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <dirent.h>
#include <fbl/algorithm.h>
#include <fbl/string_buffer.h>
#include <fbl/vector.h>
#include <fcntl.h>
#include <fuchsia/hardware/power/c/fidl.h>
#include <lib/fdio/unsafe.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/directory.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <zircon/syscalls.h>
#include <zircon/types.h>

typedef struct {
  int type;
  char name[255];
  uint8_t state;
  zx_handle_t events;
  zx_handle_t fidl_channel;
} pwrdev_t;

struct arg_data {
  bool debug;
  bool poll_events;
};

static const char* type_to_string[] = {"AC", "battery"};

zx_status_t get_source_info(zx_handle_t channel, struct fuchsia_hardware_power_SourceInfo* info) {
  zx_status_t status, op_status;

  // If either fails return the error we see (0 is success, errors are negative)
  status = fuchsia_hardware_power_SourceGetPowerInfo(channel, &op_status, info);
  zx_status_t result = fbl::min(status, op_status);
  if (result != ZX_OK) {
    fprintf(stderr, "SourceGetPowerInfo failed (transport: %d, operation: %d)\n", status,
            op_status);
  }
  return result;
}

static const char* state_to_string[] = {"online", "discharging", "charging", "critical"};
static const char* state_offline = "offline/not present";
const char* get_state_string(uint32_t state, fbl::StringBuffer<256>* buf) {
  buf->Clear();
  for (size_t i = 0; i < countof(state_to_string); i++) {
    if (state & (1 << i)) {
      if (buf->length()) {
        buf->Append(", ");
      }
      buf->Append(state_to_string[i]);
    }
  }

  return (buf->length() > 0) ? buf->c_str() : state_offline;
}

static zx_status_t get_battery_info(zx_handle_t ch) {
  struct fuchsia_hardware_power_BatteryInfo binfo = {};
  zx_status_t op_status;
  zx_status_t status = fuchsia_hardware_power_SourceGetBatteryInfo(ch, &op_status, &binfo);
  if (status != ZX_OK) {
    printf("GetBatteryInfo returned %d\n", status);
    return status;
  }

  const char* unit = (binfo.unit == fuchsia_hardware_power_BatteryUnit_MW) ? "mW" : "mA";
  printf("             design capacity: %d %s\n", binfo.design_capacity, unit);
  printf("          last full capacity: %d %s\n", binfo.last_full_capacity, unit);
  printf("              design voltage: %d mV\n", binfo.design_voltage);
  printf("            warning capacity: %d %s\n", binfo.capacity_warning, unit);
  printf("                low capacity: %d %s\n", binfo.capacity_low, unit);
  printf("     low/warning granularity: %d %s\n", binfo.capacity_granularity_low_warning, unit);
  printf("    warning/full granularity: %d %s\n", binfo.capacity_granularity_warning_full, unit);
  printf("                present rate: %d %s\n", binfo.present_rate, unit);
  printf("          remaining capacity: %d %s\n", binfo.remaining_capacity, unit);
  printf("             present voltage: %d mV\n", binfo.present_voltage);
  printf("==========================================\n");
  printf("remaining battery percentage: %d %%\n",
         binfo.remaining_capacity * 100 / binfo.last_full_capacity);
  if (binfo.present_rate < 0) {
    printf("      remaining battery life: %.2f h\n",
           (float)binfo.remaining_capacity / (float)binfo.present_rate * -1);
  }
  putchar('\n');
  return ZX_OK;
}

void parse_arguments(int argc, char** argv, struct arg_data* args) {
  int opt;
  while ((opt = getopt(argc, argv, "p")) != -1) {
    switch (opt) {
      case 'p':
        args->poll_events = true;
        break;
      default:
        fprintf(stderr, "Invalid arg: %c\nUsage: %s [-p]\n", opt, argv[0]);
        exit(EXIT_FAILURE);
    }
  }
}

void handle_event(pwrdev_t& interface) {
  zx_status_t status;
  struct fuchsia_hardware_power_SourceInfo info;
  if ((status = get_source_info(interface.fidl_channel, &info)) != ZX_OK) {
    exit(EXIT_FAILURE);
  }

  fbl::StringBuffer<256> old_buf;
  fbl::StringBuffer<256> new_buf;
  printf("%s (%s): state change %s (%#x) -> %s (%#x)\n", interface.name,
         type_to_string[interface.type], get_state_string(interface.state, &old_buf),
         interface.state, get_state_string(info.state, &new_buf), info.state);

  if (interface.type == fuchsia_hardware_power_PowerType_BATTERY &&
      (info.state & fuchsia_hardware_power_POWER_STATE_ONLINE)) {
    if (get_battery_info(interface.fidl_channel) != ZX_OK) {
      exit(EXIT_FAILURE);
    }
  }

  interface.state = info.state;
}

void poll_events(const fbl::Vector<pwrdev_t>& interfaces) {
  zx_wait_item_t* items = new zx_wait_item_t[interfaces.size()];
  for (size_t i = 0; i < interfaces.size(); i++) {
    items[i].handle = interfaces[i].events;
    items[i].waitfor = ZX_USER_SIGNAL_0;
    items[i].pending = 0;
  }

  zx_status_t status;
  printf("waiting for events...\n\n");
  for (;;) {
    status = zx_object_wait_many(items, interfaces.size(), ZX_TIME_INFINITE);
    if (status != ZX_OK) {
      printf("zx_object_wait_many() returned %d\n", status);
      exit(EXIT_FAILURE);
    }

    for (size_t i = 0; i < interfaces.size(); i++) {
      if (items[i].pending & ZX_USER_SIGNAL_0) {
        handle_event(interfaces[i]);
      }
    }
  }
}

int main(int argc, char** argv) {
  struct arg_data args = {};
  parse_arguments(argc, argv, &args);

  struct dirent* de;
  DIR* dir = opendir("/dev/class/power");
  if (!dir) {
    printf("Failed to read /dev/class/power\n");
    exit(EXIT_FAILURE);
  }

  fbl::StringBuffer<256> state_str;
  fbl::Vector<pwrdev_t> interfaces;
  while ((de = readdir(dir)) != NULL) {
    int fd = openat(dirfd(dir), de->d_name, O_RDONLY);
    if (fd < 0) {
      printf("Failed to read %s, skipping: %d\n", de->d_name, fd);
      continue;
    }

    struct fuchsia_hardware_power_SourceInfo pinfo;
    zx_handle_t ch;
    zx_status_t status;
    zx_status_t op_status;

    status = fdio_get_service_handle(fd, &ch);
    if (status != ZX_OK) {
      printf("Failed to get service handle for %s, skipping: %d!\n", de->d_name, status);
      continue;
    }

    status = get_source_info(ch, &pinfo);
    if (status != ZX_OK) {
      printf("Failed to read from source %s, skipping\n", de->d_name);
      continue;
    }

    printf("[%s] type: %s, state: %s (%#x)\n", de->d_name, type_to_string[pinfo.type],
           get_state_string(pinfo.state, &state_str), pinfo.state);

    if (pinfo.type == fuchsia_hardware_power_PowerType_BATTERY &&
        (pinfo.state & fuchsia_hardware_power_POWER_STATE_ONLINE)) {
      if (get_battery_info(ch) != ZX_OK) {
        fprintf(stderr, "Couldn't read battery information for %s, skipping\n", de->d_name);
        continue;
      }
    }

    if (args.poll_events) {
      zx_handle_t h = ZX_HANDLE_INVALID;
      status = fuchsia_hardware_power_SourceGetStateChangeEvent(ch, &op_status, &h);
      if (status != ZX_OK || op_status != ZX_OK) {
        printf("failed to get event: %d / %d\n", status, op_status);
        return status;
      }

      pwrdev_t dev = {};
      dev.type = pinfo.type;
      dev.state = pinfo.state;
      dev.fidl_channel = ch;
      dev.events = h;
      memcpy(dev.name, de->d_name, sizeof(dev.name));
      interfaces.push_back(dev);
    }
  }

  if (args.poll_events) {
    poll_events(interfaces);
  }

  return 0;
}
